package hydrap2p.logging;

import hydrap2p.ServiceLocation;
import hydrap2p.helper.ThriftServerThread;
import hydrap2p.helper.ThriftUtils;
import hydrap2p.modulemanager.ModuleControl;
import hydrap2p.servicemanager.NameTaken;

import java.io.IOException;
import java.net.BindException;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Properties;

import org.apache.log4j.PropertyConfigurator;
import org.apache.thrift.TException;
import org.apache.thrift.protocol.TBinaryProtocol;
import org.apache.thrift.transport.TSocket;
import org.apache.thrift.transport.TTransport;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.Marker;
import org.slf4j.MarkerFactory;

public class LoggingImpl implements Logging.Iface, ModuleControl.Iface {
	//Init logger
	static {
		configureLog4J();
	}
	
	//Class string name, doubling as logging marker
	private static final Marker TAG = MarkerFactory.getDetachedMarker("Logging");
	
	//Subscriber list
	private List<LoggingSubscriber> subscribers; 
	
	//Console logger for Logger problems
	private static final Logger consoleLogger = LoggerFactory.getLogger(TAG.getName());
	
	private static ThriftServerThread t;
	private static ThriftServerThread t2;
	
	// port that we're running on
	private int port;
	
	public LoggingImpl(int port) throws Exception {
		this.subscribers = new ArrayList<LoggingSubscriber>();
		this.port = port;
	}
	
	public static void configureLog4J() {
		//Configure log4j
		Properties log4jProps = new Properties();
		log4jProps.setProperty("log4j.rootLogger", "TRACE, console");
		log4jProps.setProperty("log4j.appender.console", "org.apache.log4j.ConsoleAppender");
		log4jProps.setProperty("log4j.appender.console.layout", "org.apache.log4j.PatternLayout");
		log4jProps.setProperty("log4j.appender.console.layout.ConversionPattern", "%-4r [%t] %-5p %c %x - %m%n");

		PropertyConfigurator.configure(log4jProps);
	}
	
	public static void main(String[] args) {
		int port = 25303;
		LoggingImpl logging = null;
		try {
			logging = new LoggingImpl(port);
		} catch (Exception e) {
			System.out.println("Failed to start Logging, "+e.getMessage());
			System.exit(1);
		}
		
		//Start server
		consoleLogger.info(TAG, "Starting logging server");
		
		try {
			t = ThriftUtils.initThriftServer(new Logging.Processor(logging), "Logging", port);
		} catch (BindException e) {
			consoleLogger.error(TAG, "Failed to start Logging!  Could not bind to port.");
			e.printStackTrace();
			return;
		} catch (IOException e) {
			consoleLogger.error(TAG, "Failed to start Logging!");
			e.printStackTrace();
			return;
		} catch (NameTaken e) {
			consoleLogger.error(TAG, "Failed to start Logging!  Duplicate module exists.");
			e.printStackTrace();
			return;
		}

		try {
			t2 = ThriftUtils.initThriftServer(new ModuleControl.Processor(logging), "LoggingCtl", port+1);
		} catch (BindException e) {
			consoleLogger.error(TAG, "Failed to start Logging Control!  Could not bind to port.");
			e.printStackTrace();
			return;
		} catch (IOException e) {
			consoleLogger.error(TAG, "Failed to start Logging Control!");
			e.printStackTrace();
			return;
		} catch (NameTaken e) {
			consoleLogger.error(TAG, "Failed to start Logging Control!  Duplicate module exists.");
			e.printStackTrace();
			return;
		}
		
		boolean manager = ThriftUtils.waitForManager();
		
		if(manager)
		{
			t.start();
			t2.start();
		}
		else
			consoleLogger.error(TAG, "Failed to connect to essential modules!  Exiting.");
	}
	
	@Override
	public void log(Level severity, String logEntrySource, String logMessage) throws TException {
		//Guard statement for null logEntrySource
		if(logEntrySource == null)
			logEntrySource = "null";
		
		//Guard statement for null logMessage
		if(logMessage == null)
			logMessage = "null";
		
		List<LoggingSubscriber> currentSubscribers;
		List<LoggingSubscriber> failed = new LinkedList<LoggingSubscriber>();
		Marker marker = MarkerFactory.getDetachedMarker(logEntrySource);
		Logger console = LoggerFactory.getLogger(marker.getName());
		
		synchronized(subscribers)
		{
			currentSubscribers = new ArrayList<LoggingSubscriber>(subscribers);			
		}
		
		//Deliver log event to all subscribers
		for(LoggingSubscriber service : currentSubscribers) {
			if(service.getMinimumSeverity().getValue() <= severity.getValue()) {	
				try {
					service.connection.log(severity, logEntrySource, logMessage);
				} catch (TException e) {
					// This connection has failed, remove it from the listeners
					// Need to do it with a separate list because we're currently iterating through it
					failed.add(service);
				}
			}
		}
		
		synchronized(subscribers)
		{
			subscribers.removeAll(failed);	// remove any listeners that failed
		}
		
		synchronized(console)
		{
			//Log to system error.  If they can't be arsed to use a Logging constant as severity, the message gets logged at trace level
			if (severity==null)
			{
				console.trace(marker, logMessage);
			}
			else
			{
				switch(severity) {
				case VERBOSE:
					console.trace(marker, logMessage);
					break;
				case DEBUG:
					console.debug(marker, logMessage);
					break;
				case INFO:
					console.info(marker, logMessage);
					break;
				case MISHAP:
					console.warn(marker, logMessage);
					break;
				case ERROR:
					console.error(marker, logMessage);
					break;
				case FATAL:
					console.error(marker, "FATAL ERROR");
					console.error(marker, logMessage);
					break;
				default:
					console.trace(marker, logMessage);
				}
			}
		}
	}

	@Override
	public synchronized void subscribe(ServiceLocation location, Level minimumSeverity) throws TException {
		LoggingSubscriber service = new LoggingSubscriber(location, minimumSeverity);

		List<String> interfaces=null;
		try {
			TTransport transport = new TSocket(service.getLocation().getAddress(), service.getLocation().getPort());
			transport.open();
			service.connection = new LoggingListener.Client(new TBinaryProtocol(transport));
			interfaces = service.connection.supportedInterfaces();
			
		} catch (TException e) {
			// An error happened while connecting to this listener, guess he doesn't really want to listen
			return;
		}
		
		//Ensure it is a LoggingListener
		if(!interfaces.contains("LoggingListener")) {
			consoleLogger.error(TAG, "Logging: Cannot add subscriber which does not implement LoggingListener");
			return;
		}
		
		synchronized(subscribers)
		{
			if(subscribers.contains(service)) {
				//Re-add service to update minimum severity.  (Severity not included in equality check)
				subscribers.remove(service);
				subscribers.add(service);
			}
			else
				subscribers.add(service);
		}
	}

	@Override
	public void unsubscribe(ServiceLocation location) throws TException {
		LoggingSubscriber service = new LoggingSubscriber(location, Level.VERBOSE);
		
		//If service with the same location exists, remove it
		synchronized(subscribers) {
			subscribers.remove(service);
		}
	}

	@Override
	public void ping() throws TException {
		
	}

	public List<LoggingSubscriber> getSubscribers() {
		return subscribers;
	}

	public Logger getLogger() {
		return consoleLogger;
	}
	
	@Override
	public List<String> supportedInterfaces() throws TException {
		List<String> temp = new ArrayList<String>();
		temp.add("Logging");
		return temp;
	}

	@Override
	public void stop() throws TException {
		consoleLogger.info(TAG, "Stopping Logging server");
		
		t.shutdown();
		t2.shutdown();
	}
}
